﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Implements a music section.
    /// </summary>
    public class MusicSectionElement : SectionElement
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="MusicSectionElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="PageElement"/>
        /// </para>
        /// <para>
        /// The page element that directly owns this section.
        /// </para>
        /// </param>
        /// <param name="interiorCreator">
        /// <para>
        /// Type: <see cref="SectionInteriorCreator"/>
        /// </para>
        /// <para>
        /// A factory method that will create the section interior.
        /// </para>
        /// </param>
        /// <param name="presentedComposition">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The composition presented in this section.
        /// </para>
        /// </param>
        /// <param name="firstPresentedMeasure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// A reference to the first measure of the presented composition that is presented in this section.
        /// </para>
        /// </param>
        /// <param name="lastPresentedMeasure">
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// A reference to the last measure of the presented composition that is presented in this section.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="packagePart"/> or <paramref name="interiorCreator"/> or <paramref name="presentedComposition"/> is <see langword="null"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="owner"/> is not <see langword="null"/>, but the <paramref name="firstPresentedMeasure"/> or the <paramref name="lastPresentedMeasure"/>
        /// is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// The <paramref name="firstPresentedMeasure"/> or <paramref name="lastPresentedMeasure"/> does not belong to the <paramref name="presentedComposition"/>.<br/>
        /// -or-<br/>
        /// The <paramref name="lastPresentedMeasure"/> has a lower number than the <paramref name="firstPresentedMeasure"/>.<br/>
        /// </exception>
        internal MusicSectionElement(XmlPackagePart packagePart, PageElement owner, SectionInteriorCreator interiorCreator, CompositionPart presentedComposition,
            MeasureElement firstPresentedMeasure, MeasureElement lastPresentedMeasure)
            : base(packagePart, owner, interiorCreator)
        {
            if (presentedComposition == null)
                throw new ArgumentNullException("presentedComposition");
            if (firstPresentedMeasure != null && firstPresentedMeasure.FindOwner<CompositionElement>() != presentedComposition.Composition)
                throw new ArgumentException("The first presented measure does not belong to the presented composition!", "firstPresentedMeasure");
            if (lastPresentedMeasure != null && lastPresentedMeasure.FindOwner<CompositionElement>() != presentedComposition.Composition)
                throw new ArgumentException("The last presented measure does not belong to the presented composition!", "lastPresentedMeasure");
            if (firstPresentedMeasure != null && lastPresentedMeasure != null && firstPresentedMeasure.Number > lastPresentedMeasure.Number)
                throw new ArgumentException("The presented measures are in a wrong order!");
            if (owner != null)
            {
                if (firstPresentedMeasure == null)
                    throw new ArgumentNullException("firstPresentedMeasure");
                if (lastPresentedMeasure == null)
                    throw new ArgumentNullException("lastPresentedMeasure");
            }

            this.composition = presentedComposition;
            this.firstPresentedMeasure = firstPresentedMeasure;
            this.lastPresentedMeasure = lastPresentedMeasure;

            compositionAttribute.Value = presentedComposition.Uri.ToString();
            firstPresentedMeasureAttribute.Value = (firstPresentedMeasure != null ? firstPresentedMeasure.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat) : "0");
            lastPresentedMeasureAttribute.Value = (lastPresentedMeasure != null ? lastPresentedMeasure.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat) : "0");

            parts = new ElementReferenceCollection<PartDescriptorElement>(this, partsElement);
            parts.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(parts_CollectionChanged);

            //Interior.CreateContent(null);
        }

        /// <summary>
        /// Creates a new <see cref="MusicSectionElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="ModelElement"/>
        /// </para>
        /// <para>
        /// The page element that directly owns this section.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// An XML element that represents this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/>, <paramref name="owner"/> or <paramref name="element"/> is <see langword="null"/>.
        /// </exception>
        internal MusicSectionElement(XmlPackagePart packagePart, ModelElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            compositionAttribute = element.Attributes[PackageVocabulary.CompositionAttribute];
            firstPresentedMeasureAttribute = element.Attributes[PackageVocabulary.FirstMeasureAttribute];
            lastPresentedMeasureAttribute = element.Attributes[PackageVocabulary.LastMeasureAttribute];

            try
            {
                composition = (CompositionPart)packagePart.Package.GetPart(new Uri(compositionAttribute.Value, UriKind.Relative));
            }
            catch (Exception e)
            {
                throw new SerializationException("Could not get the referenced composition part " + compositionAttribute.Value + "!", element.OuterXml, e);
            }

            var fn = ushort.Parse(firstPresentedMeasureAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            if (fn > 0 && fn <= composition.Composition.MeasuresCount)
                firstPresentedMeasure = composition.Composition[fn];
            else
                throw new SerializationException("The measure number " + fn + " is not in the presented composition!", element.OuterXml);

            var ln = ushort.Parse(lastPresentedMeasureAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat);
            if (ln > 0 && ln <= composition.Composition.MeasuresCount)
                lastPresentedMeasure = composition.Composition[ln];
            else
                throw new SerializationException("The measure number " + ln + " is not in the presented composition!", element.OuterXml);
            
            partsElement = element[PackageVocabulary.PartDescriptorElementName + "s", PackageVocabulary.PackageNamespaceUri];
            parts = new ElementReferenceCollection<PartDescriptorElement>(this, partsElement);

            parts.CollectionChanged += new EventHandler<NotifyCollectionChangedEventArgs>(parts_CollectionChanged);

            Interior = ModelElement.LoadElement<SectionInteriorElement>(packagePart, this, (XmlElement)InteriorElement.FirstChild);
        }

        #endregion

        #region SectionElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElementCore(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.MusicSectionElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            compositionAttribute = document.CreateAttribute(PackageVocabulary.CompositionAttribute);
            firstPresentedMeasureAttribute = document.CreateAttribute(PackageVocabulary.FirstMeasureAttribute);
            lastPresentedMeasureAttribute = document.CreateAttribute(PackageVocabulary.LastMeasureAttribute);

            element.Attributes.Append(compositionAttribute);
            element.Attributes.Append(firstPresentedMeasureAttribute);
            element.Attributes.Append(lastPresentedMeasureAttribute);

            partsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.PartDescriptorElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            element.AppendChild(partsElement);

            return element;
        }

        /// <inheritdoc/>
        internal override bool HandleBorderRequest(Internal.SectionBorderRequest request)
        {
            if (request is Internal.MusicSectionBorderRequest)
            {
                var rq = (Internal.MusicSectionBorderRequest)request;
                
                if (request.InteriorType != null && Interior.GetType() != request.InteriorType)
                    return false;
                if (rq.PresentedComposition != null && composition != rq.PresentedComposition)
                    return false;
                if (rq.Measure != 0 && (firstPresentedMeasure.Number > rq.Measure || lastPresentedMeasure.Number < rq.Measure))
                    return false;

                return Interior.HandleBorderRequest(rq);
            }
            else
                return false;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the <see cref="Parts"/> collection has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// If the change type is Add or Replace, the handler checks that the new parts belong to the presented
        /// composition and that they are not already present.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void parts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Replace)
            {
                bool invalid = false;

                foreach (PartDescriptorElement pd in e.NewItems)
                {
                    if (parts.Count(p => p == pd) > 1)
                    {
                        parts.Remove(pd);
                        invalid = true;
                    }
                    else if (pd.FindOwner<CompositionElement>() != Composition.Composition)
                    {
                        parts.Remove(pd);
                        invalid = true;
                    }
                }

                if (invalid)
                    throw new InvalidOperationException();
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the composition presented in this section.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The composition package part that contains the data of the composition
        /// presented inside this music section.
        /// </para>
        /// </value>
        public CompositionPart Composition
        {
            get { return composition; }
        }

        /// <summary>
        /// Gets or sets the first measure presented in this section.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// A reference to the first measure of the presented <see cref="Composition"/> that is presented in this section.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// <paramref name="value"/> does not belong to the presented <see cref="Composition"/>.<br/>
        /// -or-<br/>
        /// <paramref name="value"/> has a higher number than the <see cref="LastPresentedMeasure"/>.
        /// </exception>
        /// TODO: Write the validation rules
        public MeasureElement FirstPresentedMeasure
        {
            get { return firstPresentedMeasure; }
            set
            {
                ValidateAndThrow("FirstPresentedMeasure", value);

                firstPresentedMeasure = value;
                firstPresentedMeasureAttribute.Value = (value != null ? value.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat) : "0");

                RaisePropertyChanged("FirstPresentedMeasure");
            }
        }

        /// <summary>
        /// Gets or sets the last measure presented in this section.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="MeasureElement"/>
        /// </para>
        /// <para>
        /// A reference to the last measure of the presented <see cref="Composition"/> that is presented in this section.
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// <paramref name="value"/> does not belong to the presented <see cref="Composition"/>.<br/>
        /// -or-<br/>
        /// <paramref name="value"/> has a lower number than the <see cref="FirstPresentedMeasure"/>.
        /// </exception>
        /// TODO: Write the validation rules
        public MeasureElement LastPresentedMeasure
        {
            get { return lastPresentedMeasure; }
            set
            {
                ValidateAndThrow("LastPresentedMeasure", value);

                lastPresentedMeasure = value;
                lastPresentedMeasureAttribute.Value = (value != null ? value.Number.ToString(PackageVocabulary.PackageCulture.NumberFormat) : "0");

                RaisePropertyChanged("LastPresentedMeasure");
            }
        }

        /// <summary>
        /// Gets the collection of parts presented in this section.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ElementCollection{T}"/> of <see cref="ElementReference{T}"/> of <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The parts of the presented <see cref="Composition"/> that are presented in this music section.
        /// </para>
        /// </value>
        public ElementReferenceCollection<PartDescriptorElement> Parts
        {
            get { return parts; }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Composition"/> property.
        /// </summary>
        private CompositionPart composition;
        /// <summary>
        /// Holds the XML attribute that holds the URI of the adapted composition part.
        /// </summary>
        private XmlAttribute compositionAttribute;
        /// <summary>
        /// Holds the value of the <see cref="FirstPresentedMeasure"/> property.
        /// </summary>
        private MeasureElement firstPresentedMeasure;
        /// <summary>
        /// Holds the XML attribute that holds the number of the first presented measure.
        /// </summary>
        private XmlAttribute firstPresentedMeasureAttribute;
        /// <summary>
        /// Holds the value of the <see cref="LastPresentedMeasure"/> property.
        /// </summary>
        private MeasureElement lastPresentedMeasure;
        /// <summary>
        /// Holds the XML attribute that holds the number of the last presented measure.
        /// </summary>
        private XmlAttribute lastPresentedMeasureAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Parts"/> property.
        /// </summary>
        private ElementReferenceCollection<PartDescriptorElement> parts;
        /// <summary>
        /// Holds the XML element that holds the <see cref="parts"/> collection.
        /// </summary>
        private XmlElement partsElement;

        #endregion
    }
}
